# Copyright 2009 Larry Resnik, Lasfraka-Elixir

#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.

#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import division

import random
from math import pi
import pyglet
from pyglet.window import key
from pyglet.gl import (glTranslatef, glPushMatrix, glPopMatrix, glScalef,
                       glLoadIdentity, glColor4f, glRectf, glClear, glColor3f,
                       GL_COLOR_BUFFER_BIT)
import thing
from level import Level
from camera import Camera
from bullet import BulletHandler, bulletAssociations
from physics import rect_rect_collide, COLLIDE_TYPE_RECT
import os


# This is the minimum time before you can force a fade-out during death.
DEATH_ALLOW_FORCED_RESTART = 10
# This is the point in time when the fadeout occurs.
DEATH_END = 200

# This is the true framerate to hit.
SKIP_TICKS = 1.0 / 60.0
# This is how many update iterations may be made when we're behind due to lag.
MAX_FRAMESKIP = 10


class ObjectMapIndividual(object):
    """This is a single object in the level map and it's properties.

    It is not meant to hold a game's live object. It is made to be sorted based
    on its x coordinate.
    """

    def __init__(self, type, x, y, facingRight=True):
        """type is a string representing kind of object this is."""

        self.type = type
        self.x = x
        self.y = y
        self.facingRight = facingRight
        # When an object instance is on-screen at any time, this is True to
        # disallow respawning while within the respawn location's activation
        # spot.
        self.liveFlag = False

    def __cmp__(self, other):
        if self.x > other.x:
            return 1
        elif self.x < other.x:
            return -1
        else:
            return 0

class NoDraw(object):
    """Placeholder for things that could be drawn but are unavailable."""
    def draw(self):
        pass

class GameScene(object):
    def __init__(self, manager):
        self.manager = manager
        self.thisGameTick = 0.0
        self.nextGameTick = 0.0

        self.level = Level(os.path.normpath('res/stage/prologue.tmx'))
#        self.level.add_extraneous_tiles(500)
        self.camera = Camera(self.level.mapWidth, self.level.mapHeight)
        self.background = NoDraw()

        self.objectBatch = pyglet.graphics.Batch()
        self.playerBullets = BulletHandler(bulletAssociations,
            self.objectBatch, self.level.groups[0])
        self.enemyBullets = BulletHandler(bulletAssociations, self.objectBatch,
            self.level.groups[0])
        if manager.settings.character == 'arwing':
            self.player = thing.Arwing(self.manager, self.playerBullets, 0, 0)
        else:
            self.player = thing.Marisa(self.manager, self.playerBullets, 0, 0)
        self.player.lookAtari = False

        # This holds the objects that could be in existence.
        self.objectMap = []
        # The dict key for these is the index in objectMap.
        self.objectInstances = {}
        self.aimable = {'p':self.player}
        self._parse_map_properties(self.level.levelMap.properties)
        self.healthLabel = pyglet.text.Label(str(self.player.health),
            font_size=20, x=160, y=220)

        self.frail = self.manager.settings.properties['frail']
        self.debug = self.manager.settings.properties['debug']
        if self.debug:
            self._init_debug_labels()
            self.player.debug_text = self.debugFrame
        # This is a cheesy fix for my accidental double-update. The
        # intro_sequence updates because of the Manager's update after a scene
        # switch, but not because of a death restart.
        self.first_start = True
        self.intro_sequence()
        self.first_start = False

    def _init_debug_labels(self):
        self.debugLabelBatch = pyglet.graphics.Batch()
        batch = self.debugLabelBatch
        Label = pyglet.text.Label
        y = 240
        self.debugFrame = Label('Frame', font_size=8, y=y, batch=batch)
        self.debugPosition = Label('Position',font_size=8,y=y-15,batch=batch)
        self.debugSpeed = Label('Speed', font_size=8, y=y-30, batch=batch)
        self.debugOffset = Label('Offset', font_size=8, y=y-45, batch=batch)
        self.debugBullet = Label('Bullet', font_size=8, y=y-75, batch=batch)
        self.objectCount = len(self.objectInstances)
        self.debugObject = Label('Objects: %i' % self.objectCount,
            font_size=8, y=y-60, batch=batch)

    def _init_objects(self):
        for obj_group in self.level.levelMap.object_groups:
            for obj in obj_group.objects:
                if obj.type == 'player':
                    self.player.x = obj.x
                    self.player.y = self.level.flip_y(obj.y)
                elif obj.type == 'metool':
                    self.objectMap.append(ObjectMapIndividual('metool', obj.x,
                        self.level.flip_y(obj.y), True))
        self.objectMap.sort()

    def _add_object(self, objID=None, dynamicObj=None):
        """Creates an object based on its id.

        If id is not filled in or it is input as None, then the dynamicObj is
        added to the dict of object instances with a unique id of its own.
        dynamicObj is ignored if id is given a value.
        """

        if objID is not None and objID in self.objectInstances:
            # This object already exists.
            return

        # Make an object that didn't exist on the map.
        if objID is None:
            # Get an id as far to the right of the known ids as possible.
            objID = max(len(self.objectMap),
                max(self.objectInstances) + 1 if self.objectInstances else 0)
            dynamicObj.id = objID
            self.objectInstances.update({objID:dynamicObj})
#            print('Made', id, 'which is', dynamicObj)
        else:
            self.objectInstances.update({objID:self._make_object(objID)})

    def _make_object(self, id):
        """Returns an object based on id.

        The object must exist in the level map beforehand.
        """

        mapObj = self.objectMap[id]
        if mapObj.type == 'metool':
            return thing.Metool(self.manager, self.enemyBullets, id, mapObj.x,
                mapObj.y, self.objectBatch)

    def _parse_map_properties(self, properties):
        """Acts on the dict of properties set in the map."""
        intro_tune = None
        loop_tune = None
        for prop in properties:
            if prop == 'background':
                try:
                    backgroundImage = pyglet.resource.image(properties[prop])
                except pyglet.resource.ResourceNotFoundException:
                    continue
                self.background = pyglet.sprite.Sprite(backgroundImage, 0, 0)
                self.background.scale = max(320.0 / self.background.width,
                                            240.0 / self.background.height)
            elif prop == 'bgm':
                loop_tune = properties[prop]
            elif prop == 'bgm_intro':
                intro_tune = properties[prop]
        self.manager.set_bgm(intro_tune, loop_tune)

    def restart_game(self, dt=0):
        """Cleans up memory in the current game and restarts the level."""
        self.manager.updating = False

        # Delete all memory.
        self.playerBullets.empty()
        self.enemyBullets.empty()
        for obj in list(self.objectInstances.itervalues()):
            obj.remove()

        # BGM start
        pyglet.clock.schedule_once(self.manager.request_fade, 0.0,
            True, self.intro_sequence)

    def intro_sequence(self, dt=0.0):
        """Puts the game in a state where the player enters a level."""
        # Recreate the game.
        self.player.revive()
        if self.frail:
            self.player.health = 1
        self.healthLabel.text = '%s' % self.player.health
        self._init_objects()
        # This is how long the player has been dead. Used to delay the restart.
        self.deathTime = 0
        # This is eye candy that determines where the extra death circles go.
        self.deathCircleAngle = 0
        self.manager.bgm_player.start_sound()
#        self.manager.set_bgm()
        if not self.first_start:
            pyglet.clock.schedule_once(self.manager.request_fade, 0.0,
                False, self.manager.start_updating)

    def cleanup(self):
        print('GameScene cleanup.')
        # TODO: Delete more shit.
        if self.debug:
            self.debugFrame.delete()
            self.debugPosition.delete()
            self.debugSpeed.delete()
            self.debugOffset.delete()
            self.debugBullet.delete()
        self.playerBullets.empty()
        self.enemyBullets.empty()
        # Make a copy of the list or else Python'll complain about us removing
        # items while going through the list.
        for obj in list(self.objectInstances.itervalues()):
            obj.remove()
        #Metool.image = None
        #Player.image = None

    def event_player_death(self, keys):
        """Event to be constantly called when player is killed.

        Mayber a scheduled event would be better but I don't know.
        """

        if not self.deathTime:
            # TODO: BGM stop
            for ang in range(9):
                self._add_object(None, thing.DeathCircle(self.manager, None,
                    self.player.x, self.player.y, angle=pi / 4.0 * ang))
            self.objectCount = len(self.objectInstances)
            self.debugObject.text = 'Objects: %s' % self.objectCount
        elif self.deathTime <= 138 and not self.deathTime % 6:
            self._add_object(None, thing.DeathCircle(self.manager, None,
                self.player.x, self.player.y, speed=5,
                angle=self.deathCircleAngle))
            self.deathCircleAngle += pi / 5.0
            self.objectCount += 1
            self.debugObject.text = 'Objects: %s' % self.objectCount
        elif self.deathTime == DEATH_END:
            self.restart_game()
        self.deathTime += 1

    def on_draw(self):
        offsetX = self.camera.determine_visible_x(self.player.x)
        offsetY = self.camera.determine_visible_y(self.player.y)
        glLoadIdentity()
        glScalef(self.manager.settings.properties['scale'] / 100,
                 self.manager.settings.properties['scale'] / 100, 1.0)
        self.manager.window.clear()
        # All things are drawn in the opposite direction of the player's
        # progression. Things on-screen must be drawn based on the player's
        # position around the center of the screen.
        self.background.draw()
        self.level.draw(offsetX - self.player.x, offsetY - self.player.y)

        # Draw player.
        if self.player.state != 'death':
            glPushMatrix()
            glTranslatef(offsetX, offsetY, 0.0)
            self.player.draw()
            glPopMatrix()

        # Draw objects.
        glPushMatrix()
        glTranslatef(offsetX - self.player.x + 160,
            offsetY - self.player.y + 120, 0.0)
        self.objectBatch.draw()
        for obj in self.objectInstances.itervalues():
            obj.draw()
        glPopMatrix()

        # Draw the HUD.
        self.healthLabel.draw()
        if self.debug:
            self.debugOffset.text = 'Offset: %i, %i' % (offsetX, offsetY)
            self.camera.draw()
            self.debugLabelBatch.draw()

    def _revive_nearby_objects(self):
        """Creates objects that are out-of-play based on player position."""
        for objID, obj in enumerate(self.objectMap):
            # Note: 320 / 2 + 32 + 1 = 193; 240 / 2 + 32 + 1 = 153
            # 320 is the screen width. 32 is the width of a tile. 1 is padding.
            if obj.x >= self.player.x - 193 and \
                obj.x <= self.player.x + 193 and \
                obj.y >= self.player.y - 153 and \
                obj.y <= self.player.y + 153:
                if not obj.liveFlag:
                    self._add_object(objID)
                    obj.liveFlag = True
                    if self.debug:
                        self.objectCount += 1
                        self.debugObject.text = 'Objects: %s'%self.objectCount
            elif objID not in self.objectInstances:
                # obj is off-screen and dead. Let it be able to respawn.
                obj.liveFlag = False

    def iterate_scene(self, keys):
        """Does one update cycle for this Scene."""
        player = self.player
        offsetX = self.camera.determine_visible_x(player.x)
        offsetY = self.camera.determine_visible_y(player.y)
        sendX = offsetX + player.x - 160
        if offsetX < 0:
            sendX = 0
        elif offsetX > 0:
            sendX = self.camera.mapWidth * 32 - 320
        sendY = offsetY + player.y
        if offsetY < -32:
            sendY = 64
        elif offsetY > -32:
            sendY = self.camera.mapHeight * 32 - 240 + 48

        # Update objects.
        if not player.invincibleTime:
            for obj in self.objectInstances.itervalues():
                if obj and obj.collideType == COLLIDE_TYPE_RECT and \
                    rect_rect_collide(player, obj):
                    player.hit(obj)
            self.enemyBullets.update([self.player], sendX, sendY)
        else:
            self.enemyBullets.update([], sendX, sendY)
        self.playerBullets.update(self.objectInstances.values(), sendX, sendY)
        for obj in self.objectInstances.itervalues():
            obj.update(sendX, sendY, self.aimable, self.level)

        # Update player.
        if player.state != 'death':
            player.update(self.level, keys)
            self._revive_nearby_objects()
        else:
            self.event_player_death(keys)
        if self.player.state in ['hit', 'death']:
            self.healthLabel.text = str(self.player.health)

        # Remove dead objects.
        for deadObject in [obj for obj in self.objectInstances.itervalues() \
            if obj and obj.dead]:
#            print('removing', deadObject)
            deadObject.remove()
            if self.debug:
                self.objectCount -= 1#len(self.objectInstances)
                self.debugObject.text = 'Objects: %s' % self.objectCount

        if keys.statuspause.held == 1 and not self.deathTime:
            pass
#            pyglet.clock.unschedule(self.update)
#            pyglet.clock.schedule_once(self.manager.switch_scene, 0.0,
#                                       'weapon')
        if self.deathTime > DEATH_ALLOW_FORCED_RESTART and \
            self.deathTime < DEATH_END - 2:
            if keys.shoot.held == 1 or keys.special.held == 1:
                # Why -2? I dunno.
                self.deathTime = DEATH_END - 2

        if self.debug:
            self.debugBullet.text = 'Bullets: %i' % \
                (self.enemyBullets.count + self.playerBullets.count)
            self.debugPosition.text = 'Position: %i, %i' % (player.x, player.y)
            self.debugPosition.text = 'Double Shot: %i' % \
                keys.shoot.is_double_tap()
            self.debugSpeed.text = 'Speed: %i, %i' % (player.xSpeed,
                player.ySpeed)
